package com.googlecode.cswish.struts.hibernate;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.MappedSuperclass;
import javax.persistence.spi.PersistenceProvider;
import javax.persistence.spi.PersistenceUnitInfo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.cfg.Configuration;
import org.hibernate.ejb.Ejb3Configuration;
import org.hibernate.ejb.HibernatePersistence;
import org.hibernate.ejb.packaging.NativeScanner;

import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;

//BAD design ! But I need the reference to Configuration & sessionFactory
public class HibernateJpaVendorAdapter extends
		org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter {
	
	private static final Log logger = LogFactory.getLog(HibernateJpaVendorAdapter.class);
	
	// But the default spring provider doesn't keep the referece to Ejb3Configuration 
	// if there's not JNDI lsupport
	private static Map<String, Configuration> configurations = new HashMap<String, Configuration>();
	private static Map<String, String> classProductNames = new HashMap<>();
	
	/*
	private final PersistenceProvider provider = new HibernatePersistenceProvider() {
		public EntityManagerFactory createEntityManagerFactory(String persistenceUnitName,
				Map properties) {
			try {
				final EntityManagerFactoryBuilder builder = getEntityManagerFactoryBuilderOrNull( persistenceUnitName, properties );
				if ( builder == null ) {
					return null;
				}
				else {
					Configuration cfg = ((EntityManagerFactoryBuilderImpl)builder).getHibernateConfiguration();
					EntityManagerFactory factory = builder.build();
					configurations.put(persistenceUnitName, cfg);
					return factory;
				}
			}
			catch (PersistenceException pe) {
				throw pe;
			}
			catch (Exception e) {
				throw new PersistenceException( "Unable to build entity manager factory", e );
			}
		}

		public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
			return createContainerEntityManagerFactory(info, properties);
		}
	};
	
	public EntityManagerFactory getHibernateConfiguration(PersistenceUnitInfo info, Map properties) {
		EntityManagerFactoryBuilderImpl builder = (EntityManagerFactoryBuilderImpl)
			Bootstrap.getEntityManagerFactoryBuilder( info, properties );
		EntityManagerFactory factory = builder.build();
		configurations.put(info.getPersistenceUnitName(), builder.getHibernateConfiguration());
		return factory;
	}*/
	
	private final PersistenceProvider provider = new HibernatePersistence() {
		public EntityManagerFactory createEntityManagerFactory(String persistenceUnitName,
				Map overridenProperties) {
			Ejb3Configuration cfg = new Ejb3Configuration();
			Ejb3Configuration configured = cfg.configure( persistenceUnitName, overridenProperties );
			
			Configuration configuration = configured.getHibernateConfiguration();
			configurations.put(persistenceUnitName, configuration);
			initAnnotationClass(overridenProperties, configuration);
			
			return configured != null ? configured.buildEntityManagerFactory() : null;
		}

		public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info,
				Map map) {
			return createManagerFactory(info, map);
		}
	};
	
	public EntityManagerFactory createManagerFactory(PersistenceUnitInfo info,
			Map map) {
		Ejb3Configuration cfg = new Ejb3Configuration();
		Ejb3Configuration configured = cfg.configure( info, map ); 
		
		Configuration configuration = configured.getHibernateConfiguration();
		configurations.put(info.getPersistenceUnitName(), configuration);
		
		initAnnotationClass(info.getProperties(), configuration);		
		return configured != null ? configured.buildEntityManagerFactory() : null;
	}

	private void initAnnotationClass(Map properties,
			Configuration configuration) {		
		// FIXME: call DynamicModelManager.enableCrossSite
		
		// set the default product name
		List<XClass> result = getMetadataSourceQueue(configuration);
		if (result != null && result.size() > 0) {
			for (XClass xclass : result) {
				String className = xclass.getName();
				int beginIndex = FrameConstant.PACKAGE_PREFIX.length();
				int endIndex = className.indexOf('.', beginIndex + 1);
				String productName = className.substring(beginIndex, endIndex);
				
				classProductNames.put(className, productName);
			}
		}
		
		Set<String> scannedUrls = new HashSet<>();
		Class coreClass = ClassUtil.forName("com.espirit.eap.sa.LoginUser");
		if (coreClass != null) {
			scanAndAddClass(configuration, coreClass, ISite.DEFAULT_NAME, scannedUrls);
		}
		
		String checkClass = (String)properties.get("cswish.scannerIdentify");
		if (checkClass != null && checkClass.length() > 0) {
			String[] checkClasses = checkClass.split(",");
			for (String theClass : checkClasses) {
				Class identifyClass = ClassUtil.forName(theClass.trim());
				if (identifyClass != null) {
					int begin = theClass.indexOf('.') + 1;
					int end = theClass.indexOf('.', begin);
					String productName = theClass.substring(begin, end);
					scanAndAddClass(configuration, identifyClass, productName, scannedUrls);
				}
			}
		}
	}

	private void scanAndAddClass(Configuration configuration,
			Class identifyClass, String productName, Set<String> scannedUrls) {
		URL url = identifyClass.getProtectionDomain().getCodeSource().getLocation();
		if (scannedUrls.contains(url)) {
			return;
		}
		
		NativeScanner scanner = new NativeScanner();
		Set<Class<? extends Annotation>> annotationsToInclude = new HashSet<Class<? extends Annotation>>(3);
		annotationsToInclude.add( Entity.class );
		annotationsToInclude.add( MappedSuperclass.class );
		annotationsToInclude.add( Embeddable.class );
		Set<Class<?>> matchingClasses = scanner.getClassesInJar(url, annotationsToInclude );
		
		for (Class clazz : matchingClasses) {
			configuration.addAnnotatedClass(clazz);
			classProductNames.put(clazz.getName(), productName);
		}
		scannedUrls.add(url.getPath());
	}
	
	private List<XClass> getMetadataSourceQueue(Configuration configuration) {
		try {
			Field field = Configuration.class.getDeclaredField("metadataSourceQueue");
			field.setAccessible(true);
			Object metadataSourceQueue = field.get(configuration);
			Field annotatedClassesField = metadataSourceQueue.getClass().getDeclaredField("annotatedClasses");
			annotatedClassesField.setAccessible(true);
			return (List<XClass>)annotatedClassesField.get(metadataSourceQueue);
		} catch (Exception ex) {
			logger.error("Failed to get hibernate class name", ex);
			return null;
		}
	}
	
	public PersistenceProvider getPersistenceProvider() {
		return this.provider;
	}
	
	public static final Map<String, Configuration> getConfigurations() {
		return configurations;
	}
	
	public static final Map<String, String> getClassProductNames() {
		return classProductNames;
	}
}